#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Logging tools for Tack.

This module has two parts.

1) Logging functions that are called by other apps.  These functions will then
print out the messages given them if the appropriate logging level is set

2) A logging app.  This can be called by a client side event (probably a
javascript call) and will print out the message on the server side

The logger may be configured either by the site config file, or by the
environment variable TACKLOG.  The parameters may be delimted by semicolons
in the environment variable.

LOGGING LEVEL
The level is optional, and defaults to DEBUG.  It may be specified:

level={DEBUG|INFO|WARNING|ERROR|CRITICAL}

HANDLER
The handler controls the output of the logger.  Specifying the handler is
optional, and defaults to the StreamHandler on sys.stderr.  The format for the
configuration is:

handler=HandlerType:named_option_1=value,named_option_2=value,...,

StreamHandler
strm: is the stream to use, specified as a Python object. E.g.: sys.stderr

FileHandler
filename: file to write
mode (optional): mode of file ('a' or 'w')

RotatingFileHandler
filename: file to write
mode (optional, defaults to 'a'): mode of file ('a' or 'w')
maxBytes (optional, defaults to no limit): Maximum size of the output file
backupCount (optional, defaults to no rotation): Maximum number of log files

TimedRotatingFileHandler
filename: file to write
when (optional, defaults to H): Specifies the type of interval for rotation
    The files will be rotated based on the product of when and interval.
    possible (case insensitive) values for when are:
    S, seconds
    M, minutes
    H, hours
    D, days
    W, week day (0 = Monday)
    midnight, roll over at midnight
interval (optional, defaults to 1): Specifies the numeric interval
backupCount (optional, defaults to no rotation): Maximum number of log files

SocketHandler
host: host to connect to
port: port to connect to

DatagramHandler
host: host to send to
port: port to send to

SysLogHandler
address (optional, defaults to localhost:514): host and port to log to.
    Must be specified as host/port in the configuration
facility (option, defaults to LOG_USER): The syslog facility to use

NTEventLogHandler
appname: Name of the app as it appears in the system log
dllname (optional, defaults to win32service.pyd): source for messages
logtype (optional, defaults to 'Application'): One of Application, System
    or Security

SMTPHandler
mailhost: host to use to send mail
fromaddr: address from which the mail will appear
toaddrs: addresses to which the mail is sent, seperated by '/'
subject: subject of the e-mail

HTTPHandler
host: host to which the log message is sent
url: url to which the logs message is sent
method (optional, defaults to GET): one of POST or GET

LOGGING FORMAT

Specified by:

format=<string with formatting placeholders>
    defaults to: '%(asctime)s - %(levelname)s %(name)s - %(message)s'

datefmt =<time format string>
    a time.strftime() compatible string, defaults to: ISO format

Both formatting options are optional

Placeholders:

%(name)s  Name of the logger (logging channel).
%(levelname)s  Text logging level for the message.
%(asctime)s  Human-readable time when the LogRecord was created. By default this
    is of the form ``2003-07-08 16:49:45,896'' (the numbers after the comma are
    millisecond portion of the time).
%(message)s  The logged message, computed as msg % args.

For complete list of possible placeholders, see the Python Logging documenation

CHANNEL NAME:
channel='a_string'

This is a string that will name the log channel created, and be output in the
log strings.  The channel name is optional, and will default to 'tack.'

EXAMPLE CONFIG

Environment Variable:

TACKLOG=level=DEBUG;handler=StreamHandler:strm=sys.stdout;channel=tack;\
format=%(asctime)s - %(message)s;dateformat=%m

In a site ini file:

logger_level=DEBUG
logger_handler=StreamHandler:strm=sys.stdout
logger_channel=tack
logger_format='%(asctime)s - %(message)s'
logger_dateformat='%m'

"""

import logging
import logging.handlers as handlers
import os
import sys

from tack.lib import import_attr, json_app
from tack.lib.jsontools import json_loads
from tack.lib.modtools import load_config

# Default config
default_cfg = {'channel': 'tack',
               'level': logging.DEBUG,
               'handler_name': 'StreamHandler',
               'handler_args': {},
               'format': '%(asctime)s - %(levelname)s %(name)s - %(message)s',
               'dateformat': None, # ISO8601
               'report': True
               }

def parse_config(params):
    cfg = default_cfg.copy()
    for p in params:
        if p.strip() == '':
            continue
        pk, pv = p.split('=', 1)
        pk = pk.strip()
        pv = pv.strip()
        if pk == 'level':
            cfg[pk] = logging._levelNames.get(pv, logging.DEBUG)
        elif pk == 'handler':
            try:
                handler_name, args = pv.split(':', 1)
                handler_name = handler_name.strip()
                args = dict([[e.strip() for e in p.split('=', 1)]
                             for p in args.split(',')])
            except (ValueError, ):
                raise RuntimeError('Malformed handler line in logger config')
            for k,v in args.items():
                try:
                    args[k] = int(v)
                except (ValueError, ):
                    pass
                # Special cases
                if k == 'strm':
                    try:
                        args[k] = import_attr(v)
                    except (AttributeError, ):
                        raise RuntimeError('Invalid stream specified for '
                                           'logger stream handler.')
                if k == 'address':
                    args[k] = v.split('/')
                    if len(args[k] > 1):
                        args[k][1] = int(args[k][1])
                if k == 'toaddrs':
                    args[k] = v.split('/')
            cfg['handler_name'] = handler_name
            cfg['hander_args'] = args
        elif pk in  'channel|format|dateformat':
            cfg[pk] = pv
    return cfg

def create_handler(handler_name, handler_args):
    try:
        handler = getattr(logging, handler_name)(**handler_args)
    except (AttributeError, ):
        try:
            handler = getattr(handlers, handler_name)(**handler_args)
        except (AttributeError, ):
            raise AttributeError('Invalid logging handler specified')
    return handler

namers = [
    lambda x: x.__name__,
    lambda x: x.__class__.__name__.split('.')[-1],
    lambda x: str(x),
]

def create_logger(channel=None, level=None, handler_name=None,
                  handler_args=None, format=None, dateformat=None, report=True):
    """
    The channel name can be a string, or a python object.  If a string is
    passed in, the contents of the string will be used as the channel name.  If
    an object is passed in (such as a class, a class instance, or a module) the
    name of the object (as returned by .__name__) will be used to name the
    channel.
    """
    if channel:
        if isinstance(channel, basestring):
            pass
        else:
            for name in namers:
                try:
                    channel = namer(channel)
                except:
                    pass
                else:
                    break
    else:
        channel = default_cfg['channel']
    logobj = logging.getLogger(channel)
    if logobj.handlers:
        return logobj
    handler = create_handler(handler_name or default_cfg['handler_name'],
                             handler_args or default_cfg['handler_args'])
    handler.setFormatter(logging.Formatter(fmt=format or default_cfg['format'],
                                           datefmt=(dateformat or
                                                    default_cfg['dateformat'])))
    logobj.addHandler(handler)
    logobj.setLevel(level or default_cfg['level'])
    logobj.propagate = 0
    if report:
        logging.debug("created log channel '%s' at level %s", channel,
                      logging._levelNames[level])
    return logobj

def init():
    """ Apply base logging configuration, and configure logger.
    If configuration is found in environment or config file, those values
    will be used.  Otherwise, the defaults will be used.

    @return configured logger object
    """
    root = logging.root
    root.setLevel(default_cfg['level'])
    formatter = logging.Formatter(default_cfg['format'],
                                  default_cfg['dateformat'])
    default_handler = create_handler(default_cfg['handler_name'],
                                     default_cfg['handler_args'])
    default_handler.setFormatter(formatter)
    root.addHandler(default_handler)
    if 'TACKLOG' in os.environ:
        cfg = parse_config(os.environ['TACKLOG'].split(';'))
    else:
        cfg_file = load_config(os.environ)
        if cfg_file:
            cfg = parse_config(['%s=%s' % (k.replace('logger_',''),v) for k,v
                                in dict(cfg_file).items()
                            if k.startswith('logger_')])
        else:
            cfg = default_cfg
    log = create_logger(**cfg)
    return log

log = init()

critical, error, warning, info, debug = (log.critical, log.error, log.warning,
                                         log.info, log.debug)

def logging_app(logger=log):
    """
    Small app to log client-side messages.  Message are sent via a form GET or
    POST with the keys "message" and "level."

    'level' is a string; one of {DEBUG|INFO|WARNING|ERROR|CRITICAL}
    'message' is a string
    """
    def app(environ, start_response):
        try:
            form = environ['form']
            message, level = form['message'], form['level']
            logger.log(logging._levelNames[level], message)
        except (KeyError, ), exc:
            e = str(exc)
            if 'level' in e or 'message' in e:
                raise KeyError('Malformed logging message passed to '
                               'logging app')
            else:
                raise
        return json_app('Message Logged')(environ, start_response)
    return app
